
import getopt
import sys
import httplib
import getpass
import base64
import urllib
import os.path
import datetime
import time
import logging
from caldavclientlibrary.protocol.webdav.definitions import davxml
from caldavclientlibrary.protocol.url import URL
from caldavclientlibrary.protocol.webdav.propfind import PropFind
from caldavclientlibrary.protocol.webdav.propall import PropAll
from caldavclientlibrary.client.clientsession import CalDAVSession
from caldavclientlibrary.protocol.http.data.string import ResponseDataString
from caldavclientlibrary.protocol.webdav.definitions import statuscodes
from caldavclientlibrary.protocol.webdav.propfindparser import PropFindParser
from caldavclientlibrary.protocol.caldav.definitions import headers
from caldavclientlibrary.protocol.http.authentication.basic import Basic
from reporttypes import ReportRange
import testbedReservation
from datetime import tzinfo


#return a timedelta obj representing amount to add to convert from local to UTC
def getUTCoffset():
	today = datetime.datetime.today()
	tutc = datetime.datetime.utcnow()
	today = today.replace( minute=0, second=0, microsecond=0)
	tutc = tutc.replace( minute=0, second=0, microsecond=0)
	return tutc - today

#returns string format for iCalendar datetime - YYYYMMDD'T'HHMMSS'Z'
def now_utc():
	return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())

#12am in the morning
#returns string format for iCalendar datetime - YYYYMMDD'T'HHMMSS'Z'
def today_utc():
	now = time.localtime()
	#change the time
	adjustedTime = time.strptime(str(now.tm_year) + ":" + str(now.tm_mon) + ":" + str(now.tm_mday) + "T" + "00" + "00", "%Y:%m:%dT%H%M")
	#convert to utc
	adjustedTime = time.gmtime( time.mktime(adjustedTime) )
	return time.strftime("%Y%m%dT%H%M%SZ", adjustedTime)

#one minute before midnight
#returns string format for iCalendar datetime - YYYYMMDD'T'HHMMSS'Z'
def today_end_utc():
	now = time.localtime()
	#change the time
	adjustedTime = time.strptime(str(now.tm_year) + ":" + str(now.tm_mon) + ":" + str(now.tm_mday) + "T" + "23" + "59", "%Y:%m:%dT%H%M")
	#convert to utc
	adjustedTime = time.gmtime( time.mktime(adjustedTime) )
	return time.strftime("%Y%m%dT%H%M%SZ", adjustedTime)

#strDate is a string in iCalendar format with timezone offset or no time
def convertToUTC( strDate, tzoffset):
	origDate = None
	if strDate.find('T') > 0:
		origDate = datetime.datetime( *time.strptime( strDate, "%Y%m%dT%H%M%S")[0:6])
	else:
		origDate = datetime.datetime( *time.strptime( strDate, "%Y%m%d")[0:6])
	timeOffset = datetime.timedelta( hours=tzoffset)
	withOffset = origDate + timeOffset
	#addition with timedelta should handle carry over between units
	return withOffset.strftime( "%Y%m%dT%H%M%SZ")

def convertDayToNum( day):
	if day == "Mon" or day == "MO":
		return 0
	if day == "Tue" or day == "TU":
		return 1
	if day == "Wed" or day == "WE":
		return 2
	if day == "Thu" or day == "TH":
		return 3
	if day == "Fri" or day == "FR":
		return 4
	if day == "Sat" or day == "SA":
		return 5
	if day == "Sun" or day == "SU":
		return 6

def incrementYear( dateObj, weekdays=None, amt=1):
	newyear = dateObj.year + amt
	return dateObj.replace(year=newyear)

def incrementMonth( dateObj, weekdays=None, amt=1):

	#todo: weekday reset to recurrence start

	newmonth = dateObj.month + amt
	if newmonth > 12:
		newmonth = newmonth - 12
		dateObj = incrementYear( dateObj)
	return dateObj.replace(month=newmonth)

def incrementWeek( dateObj, weekdays=None, amt=1):
	#offset date by number of days between given, and any byday fields
	if weekdays:
		dateStruct = time.strptime(dateObj.strftime("%Y%m%dT%H%M%SZ"), "%Y%m%dT%H%M%SZ")
		if not (dateStruct.tm_wday == weekdays[-1:][0]):
			print "Warning: Call to incrementWeek was performed before date obj matched last day of repetitions for the week"
		daysleft = 6 - dateStruct.tm_wday #Sunday=6
		nextdays = weekdays[0] + 1 #add 1 because Monday=0
		if amt > 1:
			nextdays = nextdays + (7 * (amt-1))
		return dateObj + datetime.timedelta(days=(daysleft + nextdays))
	else:
		deltaweek = datetime.timedelta( days=(7 * amt))
		return dateObj + deltaweek

def incrementDay( dateObj, amt=1):
	deltaday = datetime.timedelta( days=amt)
	return dateObj + deltaday

#def incrementDay( dateObj, amt=1):
#	strDate = dateObj.strftime("%Y%m%dT%H%M%SZ")
#	d = time.strptime( strDate, "%Y%m%dT%H%M%S")
#	newDate = time.strptime( str(d.tm_year) + ":" + str(d.tm_mon) + ":" + str(d.tm_mday + amt) + "T" + str(d.tm_hour) + ":" + str(d.tm_min) + ":" + str(d.tm_sec) , "%Y:%m:%dT%H:%M:%S")
#	return datetime.datetime( *newDate[0:6] )

def eventFactory( rawData):
	#Check if recurrence type
	evchunk = rawData.split('VEVENT', 1)[1]
	if evchunk:
		if evchunk.find('RRULE:') > 0:
			newrec = RecurringEvent(rawData)
			#Don't handle recurring events longer than a day
			oneday = datetime.timedelta(days=1)
			start = newrec.getStartDate()
			end = newrec.getEndDate()
			if (end - start) > oneday:
				return EventData(rawData)
			else:
				return newrec
		else:
			return EventData(rawData)

 
#store and display general data available for events
class EventData:

	def __init__(self, rawData=None):
		self.rawData = rawData
		self.startInfo = None
		self.endInfo = None
		self.summary = None
		self.description = None
		if rawData:
			#find whether daylight savings is in effect
			now = time.localtime()
			offset = 0
			if now.tm_isdst:
				#get dst offset
				dstchunk = self.rawData.split('BEGIN:DAYLIGHT', 1)[1]
				offset = dstchunk.split('TZOFFSETTO:',1)[1].splitlines()[0].lstrip().rstrip()
			else:
				stchunk = self.rawData.split('BEGIN:STANDARD', 1)[1]
				offset = dstchunk.split('TZOFFSETTO:',1)[1].splitlines()[0].lstrip().rstrip()
			#strip off unnecessary minutes portion
			offset = offset[:-2]
			#gCal gives offset as negative number
			offset = -1*(int(offset))
			for line in rawData.splitlines():
				if line.startswith("DTSTART;TZID") or line.startswith("DTSTART;VALUE="):
					self.startInfo = convertToUTC( line.rsplit(':',1)[1], offset)
				if line.startswith("DTEND;TZID") or line.startswith("DTEND;VALUE="):
					self.endInfo = convertToUTC( line.rsplit(':',1)[1], offset)
				if line.startswith("SUMMARY"):
					self.summary = line.split(':',1)[1]
				if line.startswith("DESCRIPTION"):
					self.description = line
				if self.startInfo and self.endInfo and self.summary and self.description:
					break

			#descMatch = eventInst.description.replace('(','\(').replace(')', '\)').lstrip().rstrip()
			#pattern = re.compile(descMatch + '(.*?)[A-Z]+:' ,flags=re.DOTALL)
			#m = pattern.search(eventInst.rawData)
			#descInfo = eventInst.description
			#if m:
			#   descInfo = descInfo + m.group(1)

			#Need to check for multi-line descriptions - only 1st line captured
			descInfo = self.description
			datalines = rawData.splitlines()
			i = 0
			for line in datalines:
				i += 1
				if line == descInfo:
					break

			while i < len(datalines):
				#look for the start of the next field to stop
				sym = datalines[i].find('-')
				if sym > 0:
					if datalines[i][:sym].isupper():
						break
					else:
						descInfo = descInfo + datalines[i].lstrip().rstrip()
						i += 1
				else:
					sym = datalines[i].find(':')
					if sym > 0:
						if datalines[i][:sym].isupper():
							break
						else:
							descInfo = descInfo + datalines[i].lstrip().rstrip()
							i += 1
					else:
						descInfo = descInfo + datalines[i].lstrip().rstrip()
						i += 1
			self.description = descInfo


	def getStartDate(self):
		return datetime.datetime.strptime(self.startInfo, "%Y%m%dT%H%M%SZ")

	def getStartDateLocal(self):
		return datetime.datetime.strptime(self.startInfo, "%Y%m%dT%H%M%SZ") - getUTCoffset()
	
	def getEndDate(self):
		return datetime.datetime.strptime(self.endInfo, "%Y%m%dT%H%M%SZ")

	def getEndDateLocal(self):
		return datetime.datetime.strptime(self.endInfo, "%Y%m%dT%H%M%SZ") - getUTCoffset()

	def __repr__(self):
		return "Start: " + self.startInfo + "\nEnd: " + self.endInfo + "\n" + self.summary


#Todo: an event that is part of a series of recurring events, when modified to another time, will 
#still have the original event info and an additional event

class RecurringEvent(EventData):

	def __init__(self, rawData=None):
		EventData.__init__(self, rawData)
		#recurrence rules
		self.rFreq = None
		self.rEnd = None #datetime obj
		self.rInterval = None
		#list of days of the week event occurs, ordered
		self.byday = None
		#number representing day of the week
		self.dayOfWeek = None
		#start time
		stringdate = self.startInfo.split("T",1)[1]
		#extract rules
		evchunk = self.rawData.split('VEVENT', 1)[1]
		if evchunk:
			if evchunk.find('RRULE:') > 0:
				rulechunk = evchunk.split('RRULE:')[1]
				rule = rulechunk.splitlines()[0].lstrip().rstrip()
				terms = dict( [keyval.split('=') for keyval in rule.split(';')])
				#find any date specifics
				if "BYDAY" in terms:
					#convert the day of the week on startInfo into a number and compare 
					#with the desired recurrence days. 0=Mon, 1=Tue ...etc
					dateAlt = time.asctime(time.strptime(self.startInfo, "%Y%m%dT%H%M%SZ") )
					self.dayOfWeek = convertDayToNum(dateAlt[:3])
					weekdays = terms["BYDAY"].lstrip().rstrip().split(",")
					self.byday = sorted([convertDayToNum(day) for day in weekdays])
				#find whether the year, month, or day is changing
				if "FREQ" in terms:
					self.rFreq = terms["FREQ"]
				else:
					print "The 'FREQ' field for the recurring event on " + self.startInfo + " was not found"
				if "COUNT" in terms:
					end = datetime.datetime.strptime( self.endInfo, "%Y%m%dT%H%M%SZ")
					#subtract 1 because count includes start day
					if self.rFreq == "DAILY":
						self.rEnd = end + datetime.timedelta(days=(int(terms["COUNT"])-1))
					elif self.rFreq == "WEEKLY":
						self.rEnd = end + datetime.timedelta(days=(7 * (int(terms["COUNT"])-1)))
					#To Do: monthly or yearly count by date


	#Take a datetime obj and return a datetime obj based on recurrence rules
	def nextRecurrence(self, dateObj):
		#check if the days of the week are through for this recurrence
		nextdate = None
		if self.byday:
			dateStruct = time.strptime(dateObj.strftime("%Y%m%dT%H%M%SZ"), "%Y%m%dT%H%M%SZ")
			if dateStruct.tm_wday < self.byday[-1:][0]:
				#find next day
				nextday = None
				for i in range(len(self.byday)):
					nextday = self.byday[i]
					if nextday > dateStruct.tm_wday:
						break
				span = nextday - dateStruct.tm_wday
				nextdate = incrementDay(dateObj, amt=span)
			else:
				#may need to change the day of week and also some other freq unit
				if self.rFreq == "WEEKLY":
					nextdate = incrementWeek(dateObj, weekdays=self.byday)
				elif self.rFreq == "MONTHLY":
					return incrementMonth(dateObj, weekdays=self.byday)
				elif self.rFreq == "YEARLY":
					return incrementYear(dateObj, weekdays=self.byday)
		else:
			#may need to change the day of week and also some other freq unit
			if self.rFreq == "DAILY":
				nextdate = incrementDay(dateObj)
			elif self.rFreq == "WEEKLY":
				nextdate = incrementWeek(dateObj)
			elif self.rFreq == "MONTHLY":
				return incrementMonth(dateObj)
			elif self.rFreq == "YEARLY":
				return incrementYear(dateObj)
		#filter based on a 'COUNT' or 'UNTIL' field
		if self.rEnd:
			if nextdate > self.rEnd:
				return None
			else:
				return nextdate
		else:
			return nextdate
			
	#This does not handle recurrence at the level of by hours, minutes, or seconds
	#Parameters are datetime objs
	def getRecurring(self, startRange, endRange):
		res = []
		recurrenceStart = datetime.datetime.strptime( self.startInfo, "%Y%m%dT%H%M%SZ")
		recurrenceInstEnd = datetime.datetime.strptime( self.endInfo, "%Y%m%dT%H%M%SZ")
		#Not 
		deltaInstance = recurrenceInstEnd - recurrenceStart
		#which is earlier, recurrence start or range start?
		mark = recurrenceStart
		deltaStart = startRange - mark
		#For deltas less than a day, the timedelta obj can use -1 day, and positive seconds.
		while deltaStart.days >= 0:
			markEnd = mark + deltaInstance
			checkdelta = markEnd - startRange
			if checkdelta.days > 0:
				break
			else:
				mark = self.nextRecurrence(mark)
				deltaStart = startRange - mark
		deltaEnd = endRange - mark
		#create as many instances of event before end of user specified range
		while deltaEnd.days >= 0:
			reEvent = EventData()
			reEvent.startInfo = mark.strftime( "%Y%m%dT%H%M%SZ")
			markEnd = mark + deltaInstance
			reEvent.endInfo = markEnd.strftime("%Y%m%dT%H%M%SZ")
			reEvent.summary = self.summary
			reEvent.description = self.description
			res.append(reEvent)
			mark = self.nextRecurrence(mark)
			if not mark:
				break
			deltaEnd = endRange - mark
		return res	



#Class to use a subset of open source caldav library
class CaldavConnection(CalDAVSession):

	def __init__(self, username, password, url, log=False):
		self.urlObj = URL(url)
		#Setting the port parameter does not satisfy how later code looks for port info
		super(CaldavConnection, self).__init__(self.urlObj.server, root=self.urlObj.relativeURL(), ssl=True, user=username, pswd=password, logging=log)

	#date parameters are datetime objs
	def getReportRange(self, startdate, enddate):
		dateEvents = []
		request = ReportRange(self, self.urlObj.relativeURL())
		#date parameters in string format for iCalendar datetime - YYYYMMDD'T'HHMMSS'Z'
		request.setRange(startdate.strftime("%Y%m%dT%H%M%SZ"), enddate.strftime("%Y%m%dT%H%M%SZ"))
		parsedResult = {}
		result = ResponseDataString()
		request.setOutput(result)
		self.runSession(request)
		if request.getStatusCode() == statuscodes.MultiStatus:
			parser = PropFindParser()
			parser.parseData(result.getData())
			parsedResult = parser.getResults()
		else:
			self.handleHTTPError(request)
		#results is a dict of href strings and the PropFindResult obj
		for (h, propResult) in parsedResult.iteritems():
			#propResult is a dict
			for (node, txt) in propResult.textProperties.iteritems():
				eventData = eventFactory(txt)
				if isinstance(eventData, RecurringEvent):
					recur = eventData.getRecurring(startdate, enddate)
					if recur:
						for reEv in recur:
							dateEvents.append( reEv)
				else:
					dateEvents.append( eventData)
		return dateEvents

	#Override fxn to skip some checking of program state that is only relevant 
	#when using caldavclientlibrary with interactive client.
	def doSession(self, request):
		if request:
			# Handle delayed authorization
			first_time = True
			while True:
				# Run the request actions - this will make any connection that is needed
				self.sendRequest(request)
				# Check for auth failure if none before
				if request.getStatusCode() == statuscodes.Unauthorized:
					if self.hasAuthorization():
						self.authorization = None
						if not first_time:
							self.displayHTTPError(request)
					self.authorization, cancelled = self.getAuthorizor(first_time, request.getResponseHeaders(headers.WWWAuthenticate))
					if cancelled:
						self.authorization = None
					else:
						first_time = False
						request.clearResponse()
						# Repeat the request loop with new authorization
						continue
				# If we get here we are complete with auth loop
				break
		self.closeConnection()

	#Override fxn
	def getAuthorizor(self, first_time, wwwhdrs):

		for item in wwwhdrs:
			if item.lower().startswith("basic"):
				return Basic(self.user, self.pswd), False
			elif item.lower().startswith("digest"):
				return Digest(self.user, self.pswd, wwwhdrs), False
			elif item.lower().startswith("negotiate") and Kerberos is not None:
				return Kerberos(self.user), False
		else:
			return None, True

#Run as a script
def run(username, password, url, caldavLogging):
	caldav = CaldavConnection(username, password, url, caldavLogging)
	#ask user for date range for events
	startRange = None
	endRange = None
	while True:
		print "Verifier options:"
		print "1 - retrieve gCal events after current date-time"
		print "2 - retrieve gCal events occuring at any time today"
		print "3 - specify a date-time range to retrieve gCal events"
		getchoice = raw_input("Please enter an option number: ")
		if getchoice == "1":
			startRange = datetime.datetime.strptime( now_utc(), "%Y%m%dT%H%M%SZ")
			endRange = datetime.datetime.strptime( today_end_utc(), "%Y%m%dT%H%M%SZ")
			break
		if getchoice == "2":
			startRange = datetime.datetime.strptime( today_utc(), "%Y%m%dT%H%M%SZ")
			endRange = datetime.datetime.strptime( today_end_utc(), "%Y%m%dT%H%M%SZ")
			break
		elif getchoice == "3":
			while True:
				try:
					start = raw_input("Enter start date (yyyymmdd'T'hhmmss): ")
					startRange = datetime.datetime.strptime( start, "%Y%m%dT%H%M%S")
					startRange = startRange + getUTCoffset()
					break
				except ValueError:
					print "The date was not in the correct format"
			while True:
				try:
					end = raw_input("Enter end date (yyyymmdd'T'hhmmss): ")
					endRange = datetime.datetime.strptime( end, "%Y%m%dT%H%M%S")
					endRange = endRange + getUTCoffset()
					break
				except ValueError as ex:
					print "The date was not in the correct format"
			break
		else:
			print "Invalid option chosen"
	requestEv = caldav.getReportRange(startRange, endRange)
	#Look at brief info about events
	print "gCal Event summaries:"
	logging.info("gCal Event summaries:")
	for ev in requestEv:
		print ev
		logging.info(str(ev))
	print "----------------"
	logging.info("------------------")

	requestRes = []
	for ev in requestEv:
		#Fill in reservations per host
		(res, err) = testbedReservation.gCalReservation(ev)
		if res:
			requestRes.extend(res)

	print "Reservations from gCal:"
	for r in requestRes:
		print r
	#sysRes = testbedReservation.sysReservation("tasks.txt")
	#print "Reservations from system:"
	#for r in sysRes:
	#	print r
	#diffReq = []
	#for r in requestRes:
	#	found = [sr for sr in sysRes if r == sr]
	#	if not found:
	#		diffReq.append(r)
	#print "Inconsistencies:"
	#for r in diffReq:
	#	print r

if __name__ == "__main__":
	options, args = getopt.getopt(sys.argv[1:], "l", ["user=","pw=","url="])
	options = dict(options)
	if not "--url" in options:
		options["--url"] = raw_input("Enter url (include http:..) ")
	if not "--user" in options:
		options["--user"] = raw_input("Enter user name: ")
	if not "--pw" in options:
		options["--pw"] = getpass.getpass()
	caldavLogging = False
	if "-l" in options:
		caldavLogging = True
	#the url for events needs to end in '/'
	if not options["--url"].endswith("/"):
		options["--url"] += "/"
	run(options["--user"], options["--pw"], options["--url"], caldavLogging)
